package com.bluebench.bigul.service.handler;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import com.bluebench.bigul.domain.Element;
import com.bluebench.bigul.domain.Element.Attribute;
import com.bluebench.bigul.domain.Element.Parameter;
import com.bluebench.bigul.domain.ElementType;
import com.bluebench.bigul.domain.Invocation;
import com.bluebench.bigul.domain.Position;
import com.bluebench.bigul.domain.Tag;
import com.bluebench.bigul.domain.Types;
import com.bluebench.bigul.exception.GraphicInitializationException;
import com.bluebench.bigul.service.TypeFactory;

/**
 * The GenericHandler is responsible for parsing the default.xml, template.xml that incldes template elements and global elements also
 * @author raghave.shukla
 *
 */

public class GenericHandler extends DefaultHandler{
	
	private Element root;
	
	private Stack<Tag> stack;
	private Stack<Element> elementStack; 
	private Stack<Attribute> attributeStack;
	
	private static boolean IS_DEFAULT;
	private static boolean IS_GLOBAL;
	private static boolean IS_COLLECTION;
	private static boolean IS_PARAM;
	private static boolean IS_ATTRIBUTE;
	private static final String ATTR_OWNER="owner";
	private static final String ATTR_USER="user";
	private static final String ATTR_ALLOW_PRINT="allowPrint";
	private static final String ATTR_ALLOW_COPY="allowCopy";
	private static final String ATTR_ID="id";
	private static final String ATTR_NAME="name";
	private static final String ATTR_TYPE="type";
	private static final String ATTR_FIELD="field";
	private static final String ATTR_REF="ref";
	private static final String ATTR_PARAM="param";
	private static final String ATTR_VALUE="value";
	private static final String ATTR_POS_LEFT="left";
	private static final String ATTR_POS_RIGHT="right";
	private static final String QNAME_ATTRIBUTE="attribute";
	private static final String QNAME_PARAM="param";
	private static final String QNAME_GLOBAL="global";
	private static final String QNAME_DEFAULT="default";
	private static final String QNAME_COLLECTION="collection";
	
			
	private static StringBuilder localCurrentId;
	private static StringBuilder globalCurrentId;
	private static StringBuilder defaultCurrentId;
		
	private Map<String,Element> globalElementsMap;	
	private Map<String,Object> defaultElementsMap;
	private Map<String,Element> templateElementsMap;
	
	private Collection<Parameter> collectionParameters;
	private int collectionCount;
	
	private Element parameterElement;
	
	public GenericHandler() {		
		
		globalElementsMap = new HashMap<String,Element>();
		templateElementsMap = new HashMap<String,Element>();
		defaultElementsMap = new HashMap<String,Object>();		
		globalCurrentId=new StringBuilder("global");
	}	
	
	@Override
	public void startDocument() throws SAXException {
		super.startDocument();
		stack = new Stack<Tag>();
		elementStack = new Stack<Element>();
		attributeStack = new Stack<Attribute>();
	}
	
	@Override
	public void startElement(String uri, String localName, String qName,Attributes attr) throws SAXException {
		
		super.startElement(uri, localName, qName, attr);
		
		switch(ElementType.getType(qName)){
		
			case GLOBAL:		
				IS_GLOBAL = true;
				break;
		
			case ELEMENT:				
			try {
				element(attr,qName);
			} catch (GraphicInitializationException e) {
				e.printStackTrace();
			}				
				break;
			
			case ATTRIBUTE:	
				attribute(attr);
				break;
		
			case PARAMETER:			
				IS_PARAM=true;
				parameter(attr);				
				break;
				
			case COLLECTION:				
				collection(attr);
				break;
				
			case DEFAULT:
				IS_DEFAULT = true;				
				break;
			
			default:	
		}
		
		System.out.println("STACK =============>"+stack);
		
	}
		 
	@Override
	public void endElement(String uri, String localName, String qName)
			throws SAXException {
		
		super.endElement(uri, localName, qName);		
		
		
		if(!elementStack.isEmpty()){
			
			Element e = elementStack.peek();
			
			if(e.getName().equals(qName)){
				
				elementStack.pop();
				stack.pop();
				
				if(IS_GLOBAL){
					trimIdentifier(globalCurrentId,e.getId());
				}if(IS_DEFAULT){
					trimIdentifier(defaultCurrentId,e.getId());
				}else{
					trimIdentifier(localCurrentId,e.getId());
				}
				
				if(IS_PARAM){
					parameterElement = e;
				}
			}
		}
		if(qName.equalsIgnoreCase(QNAME_PARAM)){
			
			if(parameterElement!=null){
				if(IS_PARAM){
					if(IS_COLLECTION){
						getLastCollectionParameter().setValue(parameterElement);
					}else{
						elementStack.peek().getLastAttribute().getLastParameter().setValue(parameterElement);
					}
				}
			}
			stack.pop();
			IS_PARAM = false;
			
			
		}else if(qName.equalsIgnoreCase(QNAME_GLOBAL)){
			
			IS_GLOBAL = false;
			
		}else if(qName.equalsIgnoreCase(QNAME_DEFAULT)){
			
			IS_DEFAULT = false;
			
		}else if(qName.equalsIgnoreCase(QNAME_COLLECTION)){
			
			collectionCount--;
			if(collectionCount==0){
				IS_COLLECTION=false;
			}
			
		}else if(qName.equalsIgnoreCase(QNAME_ATTRIBUTE)){
			attributeStack.pop();
			stack.pop();
			if(attributeStack.isEmpty()){
				IS_ATTRIBUTE = false;
			}
		}		
		
	}
	 
	private Parameter getLastCollectionParameter() {
		// TODO Auto-generated method stub
		Parameter p=null;
		if(collectionParameters instanceof ArrayList){
			ArrayList<Parameter> collParam = (ArrayList<Parameter>)collectionParameters;
			int size = collParam.size();
			 p= collParam.get(size-1);
		}
		return p;
	}

	@Override
	public void endDocument() throws SAXException {
		super.endDocument();
		elementStack = null;
		localCurrentId=null;
		globalCurrentId=null;
	}
	 
	
	/**
	 * Generates the identifier for the elements. Id for every enclosing xml element would have the id of its parent element
	 * appended before. For example for the following piece of xml, 
	 * 
	 * <font-title id="title_font" name="font" type="font">
	 *   <attribute id="color" name="color">
	 *     <param id="red" name="red" type="_int" value="0"/>
	 *   </attribute> 
	 * </font-title>
	 * 
	 * the id for the element, attribute and parameter would be 
	 * 
	 * Element Id : title_font, 
	 * Attribute Id : title_font.color, 
	 * Parameter Id : title_font.color.red 
	 *    
	 * @param currentId : id of the parent element
	 * @param id : id of the element currently processed. i.e. id mentioned in the xml
	 * @return new appended id
	 */
	private StringBuilder identifier(StringBuilder currentId,String id){
		currentId = (currentId!=null)?(currentId.append("."+id)):new StringBuilder(id);
		return currentId;
	}	
	
	
	/**
	 * All xml tags that are not of type <attribute>, <param>, <collection>, <global> or <page> become Element object. 
	 * This method creates new Element object, populates it with its tag properties (or xml attributes) like name, id, type etc. Sets its 
	 * invocation type and add the element to its corresponding stack. If the element comes from default.xml, it is added to defaultElementsStack,
	 * or added to globalElementsStack if it comes from the <global> section of your template xml or it is added to templateElementStack 
	 * in case it is child of your <page> element. Finally the element is pushed to the elementStack which holds the current element
	 * under processing.      
	 * @param attr
	 * @param qName
	 * @throws GraphicInitializationException 
	 */
	private void element(Attributes attr,String qName) throws GraphicInitializationException{
		
		Element element = new Element();		
		element.setName(qName);
		setAttributes(element, attr);
		setInvocation(element);
		
			if(IS_GLOBAL){				
				populateGlobalElements(element);			
			}else if(IS_DEFAULT){			
				populateDefaultElements(element);			
			}else if(IS_PARAM){
				Parameter p = getLastParameter();				
				p.setNested(true);
				p.setValue(element);				
			}else{		
				populateTemplateElements(element);		
			}
		
		elementStack.push(element);
		stack.push(element);
		
	}
	
	private Parameter getLastParameter() {
		if(IS_COLLECTION){
			return getLastCollectionParameter();
		}else{
			
			Tag t =stack.peek();
			if(t.getElementType()==ElementType.PARAMETER){
				return (Parameter)t;
			}else{
				Parameter p = getLastAttributeParameter();
				if(p!=null){
					return p;
				}else{
					return null;
				}
			}
			
			//Problem is here. The last parameter returned is shading rather than 
			/*Parameter p = getLastAttributeParameter();
			if(p!=null){
				return p;
			}else{
				return getLastConstructorParameter();
			}*/
		}		
	}

	private Parameter getLastConstructorParameter() {
		List<Parameter> p = elementStack.peek().getInvocation().getParameters();
		return (p.size()>0 ? p.get(p.size()-1) : null);
	}

	private Parameter getLastAttributeParameter() {
		
//		Attribute a = elementStack.peek().getLastAttribute();
		Attribute a = attributeStack.peek();
		if(a!=null){  
			return a.getLastParameter();
		}else{
			return null;
		}
		
	}

	/**
	 * There is a possibility that the parameter type is different from the nested element type. 
	 * This means that the nested element has to be passed as an argument to an object of parameter type
	 * and then will be stored as a value.
	 * @param e
	 * @param type
	 * @return valueElement : The element in which the nested element is wrapped and stored as value of parameter
	 * @throws GraphicInitializationException 
	 */
	private Element getNestedElement(Element element, String paramType) throws GraphicInitializationException {
		Element valueElement = new Element();
		Invocation invocationNested = TypeFactory.getInstance(paramType);
		Parameter invocationParam = valueElement.new Parameter();
		invocationParam.setValue(element);
		invocationNested.addParameters(invocationParam);
		valueElement.setInvocation(invocationNested);
		return valueElement;
	}

	/**
	 * Populates the Element.Attribute with corresponding xml attributes. All xml tags that are of type <attribute> are 
	 * mapped to Element.Attribute
	 * @param attr : xml attribute
	 */
	private void attribute(Attributes attr){
		Element.Attribute attribute = elementStack.peek().new Attribute();
		setAttributes(attribute, attr);
		
		if(!(stack.peek().getElementType()==ElementType.ATTRIBUTE)){			
			elementStack.peek().addAttribute(attribute);			
		}else{
			Element.Attribute a = attributeStack.peek();
			attribute.setChained(true);
			a.addAttribute(attribute);
			//elementStack.peek().addAttribute(attribute);
		}
		
		attributeStack.push(attribute);		
		stack.push(attribute);
		IS_ATTRIBUTE = true;
		
			
	}
	
	
	/**
	 * Populates the Element.Parameter with corresponding xml attributes. All xml tags that are of type parameter are 
	 * mapped to Element.Parameter.
	 * 
	 * There are 3 different cases here
	 * 
	 * Case 1 : Collection Parameters
	 * ------------------------------
	 * If the parameters are defined within the collection it would be added to a list
	 * and the list will be passed as parameter to the attribute.
	 * 
	 * Case 2 : Constructor Parameter
	 * ------------------------------
	 * If a parameter is not defined within an attribute inside an element it will be considered as a parameter 
	 * to the constructor of the element, therefore it will be stored in the invocation object as a parameter
	 * @param attr : xml attribute 
	 * 
	 * Case 3 : Attribute Parameter
	 * ---------------------------
	 * A normal case where the parameters are defined with the attribute
	 */
	private void parameter(Attributes attr){
		/*TODO : Problem is here , a collection parameter is nested with an element that has invocation param, since the IS_COLLECTION will be true  
		 * this invocation param will added to collectionParameters rather than as a invocation parameter in the else section
		 */
		
		ElementType eType = stack.peek().getElementType(); //Not a very good workaround for the above problem. To reiterate GenericHandler requires to be re-factored
		if(eType!=ElementType.ELEMENT && IS_COLLECTION ){
			collectParameters(attr);
		}
		else{
						
			Element.Parameter parameter = elementStack.peek().new Parameter();
			setAttributes(parameter, attr);
			
				if((!attributeStack.isEmpty()) && (stack.peek().getElementType()==ElementType.ATTRIBUTE)){					
					attributeStack.peek().addParameter(parameter);					
				}else{						
					Attribute lastAttribute = elementStack.peek().getLastAttribute();
					if(lastAttribute!=null){
						lastAttribute.addParameter(parameter);
					}else{
						elementStack.peek().getInvocation().addParameters(parameter);
					}	
				}
				
				stack.push(parameter);
			
		}
		
		
	}
	
	
	/**
	 * Initializes a collection to store parameters that are part of a collection. 
	 * This method is called when a <collection> is encountered 
	 * @param attr : object encapsulating <param> tag attributes
	 */
	private void collection(Attributes attr){
		IS_COLLECTION=true;
		collectionCount++;
		String type = attr.getValue(ATTR_TYPE);
				
		/*
		 * Potential Problem : The old collectionParameters containing (usa, usa_cities) which have not yet been add to last attribute are being replaced with new 
		 * ArrayList because of second collection list.
		 
		if(type.equalsIgnoreCase("arraylist")){
			collectionParameters = new ArrayList<Parameter>();
		}*/
		
		/*
		 * 1. Get Parameter from last Attribute.
		 * 2. Instantiate object of collectionParameters as arraylist object
		 * 3. Add assign collectionParameters to param value of last attribute. i.e. getLastAttribute().getLastParameter().setValue()
		 * Since value is of type object, it will set the arraylist as value. setValue must also identify update a variable isCollection=true 
		 * in the parameter.
		 * 
		 * We need to create a new addValue() method which do the following
		 * a. validate if this.isCollection==true, 
		 * b. add a new collection parameter to the value using value.add(Parameter)
		 *   
		 */
		
		collectionParameters = new ArrayList<Parameter>();
		Element e = elementStack.peek();
		Parameter p = e.new Parameter();
		p.setCollection(true);
		p.setValue(collectionParameters);
		e.getLastAttribute().addParameter(p);
		
		if(type!=null){
			if(type.equalsIgnoreCase("arraylist")){
				p.setType("arraylist");							
			}else if(type.equalsIgnoreCase("collection")){
				p.setType("collection");
			}
		}
	}
	
	/**
	 * Add current parameter to the collection
	 * @param attr : object encapsulating param attributes
	 */
	private void collectParameters(Attributes attr){
		Element.Parameter parameter = elementStack.peek().new Parameter();
		setAttributes(parameter, attr);
		collectionParameters.add(parameter);
		stack.push(parameter);
	}
	
	/**
	 * Common method for setting xml attributes for any Object that IS-A Tag i.e. Element, Attribute, Parameter and Invocation
	 * @param t : Tag element
	 * @param attr : xml attribute
	 */
	private void setAttributes(Tag t,Attributes attr){
		
		String id = attr.getValue(ATTR_ID);
		String name = attr.getValue(ATTR_NAME);
		String type = attr.getValue(ATTR_TYPE);	
		String field = attr.getValue(ATTR_FIELD);
				
		t.setId(id);	
		t.setName(name);
		t.setType(type);
		t.setField(field);		 
		
		switch(t.getElementType()){		
			
			case ELEMENT:
				
				String ref = attr.getValue(ATTR_REF);
				if(ref!=null && isExpression(ref)){
					t.setExpression(ref);
				}
				
				String owner = attr.getValue(ATTR_OWNER);
				if(owner!=null){
					t.setOwner(owner);
				}
				
				String user = attr.getValue(ATTR_USER);
				if(user!=null){
					t.setUser(user);
				}
				
				String left = attr.getValue(ATTR_POS_LEFT);
				String right = attr.getValue(ATTR_POS_RIGHT);
				if(left!=null && right!=null){
					Position p = parsePosition(left,right);
					t.setPosition(p);					
				}
				
				Boolean allowPrint = Boolean.parseBoolean(attr.getValue(ATTR_ALLOW_PRINT));
				if(allowPrint!=null){
					t.setAllowPrint(allowPrint);
				}
				
				Boolean allowCopy = Boolean.parseBoolean(attr.getValue(ATTR_ALLOW_COPY));
				if(allowCopy!=null){
					t.setAllowCopy(allowCopy);
				}			
								
				break;
				
			case ATTRIBUTE:
				
				String param = attr.getValue(ATTR_PARAM);
				if(param!=null && isExpression(param)){
					t.setExpression(param);
				}
				break;
				
			case PARAMETER:
				
				String value = attr.getValue(ATTR_VALUE);
				if(value!=null){
					if(isExpression(value)){
						t.setExpression(value);
					}
					
					if(isFormatted(value)){
						((Parameter)t).setFormatted(true);
						t.setType("paragraph");
					}
					
					((Parameter)t).setValue(value);
				}
				break;
		
		}		
		
	}	
	
	private Position parsePosition(String left, String right) {
		String[] leftStrArr =  left.split(",");
		String[] rightStrArr = right.split(",");
		Position p = null;
		try{
		 float lx = Float.parseFloat(leftStrArr[0]);
		 float ly = Float.parseFloat(leftStrArr[1]);
		 float rx = Float.parseFloat(rightStrArr[0]);
		 float ry = Float.parseFloat(rightStrArr[1]);
		 p = new Position(lx,ly,rx,ry);
		}catch(NumberFormatException e){
			System.out.println("Number Format Exception for positions "+left+","+right);
		}		
		return p;
	}

	private boolean isFormatted(String value) {
		if(value.contains(":b:")||value.contains(":i:")||value.contains(":u:")){
			return true;
		}
		return false;
	}

	/**
	 * Set the Invocation Type for the Element. Invocation refer to how an element will be invoked or created 
	 * i.e. identify the way it will be instantiated. 
	 * 
	 * There are 2 ways of deciding invocation a) using field b) using type. type based invocation refers to instantiating the 
	 * element using constructors. field based invocation applies when the element itself is nothing but a field of an enclosing class.
	 * In such cases we need to instantiate the enclosing class and then obtain the field value.
	 * 
	 * Within the xml, the field value is defined with two parts concatinated with a . (dot). 
	 * 
	 * For example : <style id="BOLD" field="style.BOLD" type="_int"/>
	 * Here field has two parts separated by a . dot. first is style which tells us the type of the enclosing class and second is BOLD
	 * which tells us the actual name of the field inside the enclosing class. By passing the enclosing type name to TypeFactory we get
	 * its Invocation object that holds all the details about how to instantiate the enclosing class.  
	 * 
	 * The Invocation object thus obtained is set in the Element object.
	 *  
	 * @param e
	 */
	private void setInvocation(Element e){
		
		String type = e.getType();
		String field = e.getField();
		
		if(field!=null && field.contains(".")){
			type = field.split("\\.")[0];
			Invocation i = TypeFactory.getInstance(type);
			if(i!=null){
			   i.setField(field);
			}
			e.setInvocation(i);	
		}else if(type!=null){
			Invocation i = TypeFactory.getInstance(type);
			e.setInvocation(i);		
		}
		
	}
	
	/**
	 * Put element to Default Elements Map. Update currentId for default Elements
	 * @param e : Element to be added
	 */
	private void populateDefaultElements(Element e){
		
		defaultElementsMap.put(e.getId(),e);
		
		defaultCurrentId = identifier(defaultCurrentId,e.getId());
		
		if(elementStack.empty()){
			root = e;
		}
	}
	
	/**
	 * Put element to Global Elements Map. Update currentId for default Elements
	 * @param e : Element to be added
	 */
	private void populateGlobalElements(Element e){
		
		globalElementsMap.put(e.getId(),e);
		
		globalCurrentId = identifier(globalCurrentId,e.getId());
		
		templateElementsMap.put(globalCurrentId.toString(),e);
	}
	
	/**
	 * Put element to Template Elements Map. Update currentId for default Elements. The first element added to
	 * template elements map becomes the root element.
	 * @param e : Element to be added
	 */
	private void populateTemplateElements(Element e){
		if(elementStack.empty()){
			
			root = e;
			
			localCurrentId=identifier(localCurrentId,e.getId());
			
			templateElementsMap.put(localCurrentId.toString(),e);
			
		}else{
			
			elementStack.peek().addElement(e);
			
			localCurrentId=identifier(localCurrentId,e.getId());
			
			templateElementsMap.put(localCurrentId.toString(),e);
			
		}
	}
	
		
	/**
	 * The current id is always maintained while processing. When a closing tag is encountered the 
	 * current id has to be trimmed of the id of the element processed currently.  
	 *  
	 * @param currentId : id of the parent element
	 * @param id : id of the element currently processed. i.e. id mentioned in the xml
	 */
	private void trimIdentifier(StringBuilder currentId,String id){		
		int start = currentId.lastIndexOf(id)-1;
		if(!(start<0)){
			int end = start + currentId.length();
			currentId.delete(start,end);
		}		
	}	
	
	/**
	 * Verify if a string is a valid expression
	 * @param ref
	 * @return boolean value true if string is an expression or else false is returned
	 */
	private boolean isExpression(String ref) {
		return (ref.startsWith("#")||ref.startsWith(":"))?true:false;		
	}
	
	/**
	 * Verifies if the key is available in the associated element map. Certain elements are referenced in xml by other elements
	 * These referenced elements have an id that is preceded by "#global" or #default depending on where the referenced
	 * element is global or default element.  
	 * @param key
	 * @return : boolean value true if key is found in the associated map  
	 */
	public boolean isKey(String key){
		
		if(key.startsWith("#global")){
			
			return globalElementsMap.containsKey(key.replace("#global.",""));
			
		}if(key.startsWith("#default")){
			
			return defaultElementsMap.containsKey(key.replace("#default.",""));
			
		}else{
			
			return templateElementsMap.containsKey(key.replace("#",""));
		}
	}
	
	
	
	public Element getRoot() {
		return root;
	}

	public void setRoot(Element root) {
		this.root = root;
	}	

	public Map<String, Element> getElementsMap() {
		return templateElementsMap;
	}	

	public Map<String, Element> getGlobalElementsMap() {
		
		return globalElementsMap;
	}

	public void setGlobalElementsMap(Map<String, Element> globalElements) {
		
		this.globalElementsMap = globalElements;
		
	}

	public Map<String, Object> getDefaultElementsMap() {
		
		return defaultElementsMap;
		
	}

	public void setDefaultElements(Map<String, Object> defaultElements) {
		
		this.defaultElementsMap = defaultElements;
		
	}
	
	public static void main(String[] args) {
		SAXParserFactory factory = SAXParserFactory.newInstance();
	    SAXParser parser;	    
	    
	    try {
	    	parser = factory.newSAXParser();
		    File file = new File("D:/Raghave/OSRMediaManager/templates/TemplatesAndDocuments/test.xml");
		    GenericHandler gh = new GenericHandler();
			parser.parse(file,gh);
		    Element e = gh.getRoot();		    
		    System.out.println();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ParserConfigurationException e) {
			
			e.printStackTrace();
		}	
	}
	
		
}
